Skip to content

fix(apply): key strict-mode baselines on (url, ref), not the source name - #2155

Merged
totalfrank merged 8 commits into
devfrom
claude/eager-fermat-gxxa0j
Sep 11, 2026
Merged

fix(apply): key strict-mode baselines on (url, ref), not the source name#2155
totalfrank merged 8 commits into
devfrom
claude/eager-fermat-gxxa0j

Conversation

@totalfrank

Copy link
Copy Markdown
Collaborator

Problem

A git source's strict-mode baseline was keyed on its display name — the from name for a named source, the substituted URL for an inline one. SourceSession.baselines was Mapping[str, str], GitSourceFetcher.fetch compared session.baseline(display) against checkout.sha, and _last_resolutions rebuilt that map from report history by SourceResolution.name.

That is the wrong key. Strict mode answers "has this repository's ref moved since this bot last resolved it", and the name the document happens to give the source is not part of that question. Four consequences, all reproduced with probe tests before the fix:

  1. A mode: strict source could never be advanced by editing the document. Changing ref — to a new tag, or to a commit SHA — keeps the same name, so the old baseline still applied and the new resolution was refused. The only escape was to flip the source to non_strict, apply once, and flip it back: disarm the pin in order to move it.
  2. manifest-schema.zh-CN.md §2.3's promise was kept by neither branch. It says a SHA-shaped ref ignores mode ("两个分支都触发不了"). In fact a strict source with a 40-hex ref was still refused against a stale baseline, and a non_strict one still got a "ref moved" note.
  3. Renaming a source dropped its baseline; re-pointing its url kept a baseline from a different repository — the second silently refusing a commit that never could have matched.
  4. Two inline sources sharing a url with different refs collapsed into one report row, because an inline display was the URL alone and SourceSession._recorded de-duplicates on the display. The report stated a sha without saying which ref produced it.

Solution

Two rules; everything else follows.

Report rows: one per declaration, identified by display. A named source's display is its from name. An inline source's display is url@ref (substituted URL, @, normalised ref — HEAD when the declaration omitted one). Two names pointing at one (url, ref) are two rows carrying the same sha; two inline declarations of one repository at two refs are two rows.

Baselines: keyed on (url, ref). SourceSession.baselines is Mapping[tuple[str, str], str], read back from report history off each row's url and ref, and looked up at the gate by (spec.url, spec.ref). The display plays no part.

Everything after that lookup is unchanged: the refusal still sits between the checkout and the adoption (so a refused move records nothing and keeps refusing), keep_last still reads the receipt filed under git+<url>@<baseline sha>:<subpath>, and the bounded walk back through report history still survives an outage without disarming the pin. A re-pinned ref now simply has no baseline — the same state a first-time source is in — so it resolves, passes, and is adopted under its new pair. A SHA-shaped ref trips neither branch by construction rather than by a special case: it only ever resolves to itself.

Supporting changes:

  • SourceResolution gains url (the substituted repository URL), carried through as_dict(), the stored-report decode, and the HTTP surface. It is report-safe by construction — this record holds names, never values.
  • The report's sources array stops being a passthrough of raw dicts on the HTTP surface: it gains a named response model (ConfigManifestApplySource) with a nullable url, the way categories and entries already have one.
  • SourceSession.checkout loses its display parameter — it only ever used it to explain that the display was the baseline key, which it no longer is.
  • Docs: manifest-schema.zh-CN.md §2.3, user-manual.zh-CN.md §4.7/§6.2/§9.7/B.2.5, and design.zh-CN.md §7.

Treated as new-feature development, with no compatibility shim. Rows written before this change carry no url, and _last_resolutions skips them: guessing which repository an old row's name meant would be inventing a pin nobody wrote. In practice this means already-stored reports contribute no baselines after deployment — the first apply per (url, ref) after the deploy re-establishes it (admitted by strict mode, as any first resolution is), and the apply after that pins normally. keep_last has no baseline receipt to reuse over that same one-apply window.

Validation

cd src/backend
uv run --no-sync python -m pytest -q -o addopts="" -W ignore \
  tests/community/core/bot_config_manifest/ \
  tests/community/adapters/http/openapi_v1/test_config_manifest_apply_bars.py
# 1081 passed

Also green: the whole tests/community/adapters/http/openapi_v1/ tree (1657 passed, 2 skipped), tests/community/endpoints/test_openapi_config_manifest_apply.py and test_openapi_create_with_manifest.py, and ruff check over the touched trees (the one finding, an unused import in schema/entries.py, is pre-existing and untouched here). uv.lock is not in the diff.

Tests added — the strict gate, in apply/test_source_resolver.py:

  1. strict, same (url, ref), git resolves a different sha → refused, nothing adopted (existing behaviour, now re-asserted under the new key).
  2. strict, ref changed in the declaration with a baseline under (url, old_ref) → passes, adopts (url, new_ref) → sha, no note.
  3. strict, url re-pointed, same ref → passes, adopts under the new url.
  4. strict, ref is a 40-hex SHA equal to what git resolves, baseline under (url, that_sha) → passes, no note.
  5. non_strict, ref changed → passes with no "ref moved" note.
  6. non_strict, same (url, ref), sha differs → passes with the note (existing behaviour).
  7. two inline declarations of one url at refs a and b → two rows, named url@a and url@b.
  8. two named sources over one (url, ref) → two rows (one per name), same sha, one checkout, one baseline key.
  9. _last_resolutions over a fake report history (apply/test_apply_service_lifecycle.py): several rows collapse to one key per (url, ref), a second ref on the same repository keeps its own key, and a row with url=None is ignored. Newest-wins per key is re-asserted by the existing walk-back tests.
  10. keep_last on the git road after a ref change: no baseline under the new key → no fallback → the fetch failure stands.
  11. an inline source's report row and its strict-refusal message both carry url@ref as the name.

Plus test_source_session.py's baseline() test rewritten against the tuple key (same repo/another ref, and same ref/another repo, both yield None). Every existing test building baselines={"src": …} moved to the tuple key, and every assertion on a SourceResolution or a report sources row now expects url.

Compatibility and risk

Wire-additive: the report's sources rows gain a url field; no field is removed or renamed. The one behavioural discontinuity is the deliberate absence of a fallback described above — strict mode is disarmed for exactly one apply per (url, ref) on first deploy, and re-arms by itself. No change to the checkout cache (already keyed on (url, ref)), to receipt URLs, or to keep_last semantics beyond which key finds the baseline sha.

Spec

src/backend/docs/bot-config-manifest/manifest-schema.zh-CN.md §2.3 — updated here to state that the baseline is per (url, ref), that changing either is a re-pin (no baseline, no refusal, no note), and that a SHA-shaped ref therefore never trips either branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje


Generated by Claude Code

totalfrank and others added 5 commits September 11, 2026 14:30
A SourceResolution named the source and the ref it declared, but not the
repository those belonged to. The report could therefore say "content
resolved to 7c1d…" without saying what "content" was, and anything reading
the report back had no way to ask "is this the same repository we resolved
last time".

Adds `url` — the substituted repository URL — to SourceResolution, its wire
shape, and the decode that reads a stored report back. It is report-safe by
construction: this record carries names, never values, and the URL is the
document's own after `${BOT_*}` substitution.

The report's `sources` array also stops being a passthrough of raw dicts on
the HTTP surface: it gains a named response model, so every field a caller
can read is spelled out there, the way `categories` and `entries` already
are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
Strict mode answers "has this repository's ref moved since this bot last
resolved it". The name the document happens to give the source is not part
of that question, and keying the baseline on it made the answer wrong in
four ways:

* A `mode: strict` source could never be advanced by editing the document.
  Changing `ref` to a new tag — or to a commit SHA — keeps the name, so the
  old baseline still applied and the new resolution was refused. The only
  way out was to flip the source to `non_strict`, apply once, and flip it
  back, which is to say: to disarm the pin in order to move it.
* The schema doc's promise that a SHA-shaped `ref` trips neither branch was
  not kept by either branch: such a source was still refused against a
  stale baseline under strict, and still got a "ref moved" note under
  non_strict.
* Renaming a source dropped its baseline; re-pointing its `url` kept a
  baseline belonging to a different repository.

The baseline map is now `(url, ref) -> sha`, read back off each report row's
`url` and `ref` and looked up at the gate by the spec's own pair. Everything
after that lookup is unchanged: the refusal still happens before adoption,
`keep_last` still reads the receipt filed under the baseline sha, and the
walk back through report history still lets an outage pass without disarming
the pin. A re-pinned `ref` now simply has no baseline, which is the same
state a first-time source is in, so it passes and is adopted under its new
pair.

Rows carrying no `url` contribute no baseline: guessing which repository an
old row's name meant would be inventing a pin nobody wrote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
The report carries one row per declaration. An inline source has no `from`
name to report under, and the URL alone was not an identity: two entries
reading one repository at two refs de-duplicated onto a single row that named
neither ref, so the report said a sha without saying which ref produced it.

An inline source is now named `<url>@<ref>`, with the ref already normalised
to `HEAD` when the declaration omitted one. Strict refusals read better for
the same reason — the message names the ref it refused, not just the
repository.

`SourceSession.checkout` loses its `display` parameter. It never used it
except to explain itself: the display was the baseline key, and it no longer
is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
The schema doc promised that a SHA-shaped `ref` trips neither branch of
`mode`, and did not say what the baseline was actually keyed on. Both now
follow from one sentence: the baseline is per `(url, ref)`, so changing
either is a re-pin — no baseline, no refusal, no note — and a SHA-shaped ref
can never trip either branch because it only ever resolves to itself.

Also documents how a `strict` source is advanced (edit the ref; there is no
flip to `non_strict` and back), and what the report's `sources` rows now
carry: a `url` field, one row per declaration, and `url@ref` as the name of a
source written inline on an entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
`_last_resolutions`' new docstring pushed
`config_manifest_apply_service.py` from 996 to 1013 lines, over the
1000-line cap `tests/community/architecture/test_no_oversized_modules.py`
enforces. Says the same things in fewer words; the longer reasoning about
why a baseline is keyed on the pair already lives in `apply/source_session`,
which is where it belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
@totalfrank
totalfrank marked this pull request as ready for review September 11, 2026 15:18

@totalfrank totalfrank left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff (5 commits, dd835cd..4a576af) against merge-base 9b7970a. Diff is clean — no findings.

What I checked:

  • Correctness: the (url, ref) key rewrite in source_session.py, source_fetchers.py, config_manifest_apply_service.py. spec.ref defaults to "HEAD" (never None) so the fetcher's session.baseline(spec.url, spec.ref) lookups line up with _last_resolutions's source.ref or "HEAD" normalization on the read-back side; strict-gate and keep_last both read the baseline by the same pair now, and checkout() dropping display doesn't strand any other caller (source_fetchers.py is the only call site).
  • Contract breakage: SourceResolution.url is additive (= None default), apply_report_codec.report_from_payload decodes it back, and ConfigManifestApplySource (new named response model, replacing list[dict]) has fields exactly matching SourceResolution.as_dict(), so apply_payload's ConfigManifestApplySource(**source) unpacking is safe. Existing endpoint test only asserts "sources": [] for the empty case, unaffected by the model swap.
  • Docs: manifest-schema.zh-CN.md §2.3, user-manual.zh-CN.md, design.zh-CN.md all describe the (url, ref) keying and url@ref inline naming consistently with the code.
  • Test coverage: new tests cover the strict-refuse/re-pin/re-point/SHA-ref/non-strict-no-note matrix in test_source_resolver.py, the report round-trip (_last_resolutions skipping url-less legacy rows, collapsing shared-key rows) in test_apply_service_lifecycle.py, and the tuple-key baseline() lookup in test_source_session.py.
  • Architecture boundaries / error handling / concurrency: not implicated by this diff — no transport, DI, or concurrency changes; only key-shape and a report field addition.

Generated by Claude Code

@totalfrank

Copy link
Copy Markdown
Collaborator Author

Review round 1 — no issues flagged.

Reviewed 4a576af (all 5 commits, dd835cd..4a576af, against merge-base 9b7970a). The automated review returned no findings, and there are no inline review threads open.

CI is green on that same SHA — all 12 check runs succeeded, including Backend unit tests (the full ~18.5k-test suite under coverage) and Singlebox coverage.

Nothing fixed and nothing disputed this round, so there is no follow-up push and no round 2 will open on its own. The PR is waiting on its human reviewers (@FreddieSun, @xianmuyq); mergeable_state is blocked for that reason and not for a conflict — the base sha matches dev's head exactly.

Two things from the implementation worth a reviewer's eye, both deliberate and both already described in the PR body:

  • No compatibility shim for reports written before this change. _last_resolutions skips rows with no url, so strict mode is disarmed for exactly one apply per (url, ref) after deploy, then re-arms itself. This follows the "no compatibility fallback" decision in the task; inferring a repository from an old row's name would invent a pin nobody wrote.
  • config_manifest_apply_service.py now sits at exactly 1000 lines, the cap enforced by tests/community/architecture/test_no_oversized_modules.py. It was 996 before this change; commit 5 trimmed the new docstring to fit. The next line added to that file trips the guard, so it is a genuine split candidate — out of scope here.

Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a576af14a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Keying baselines on `(url, ref)` alone let one declaration of a repository
disarm another's pin, which a Codex review caught and a probe test confirmed.

A document may legally declare the same `(url, ref)` twice — once `strict`,
once `non_strict` — when some entries may follow a branch and one may not.
When the ref then moves, the lax declaration delivers the new commit and
records it while the pinned one refuses. Sharing a baseline, that recorded
sha became the pin's baseline, so the very next apply — with nothing in the
document changed — handed the pinned entry the commit it had just rejected.
That is strict mode degraded to "refuse each move exactly once, then deliver
it": the failure the adopt-after-the-gate ordering exists to prevent, coming
in sideways through a second declaration one apply later.

`mode` joins the key, because a pin may only be advanced by an apply that
stood behind it under the same mode. The two declarations keep separate
histories: the lax one advances and notes its moves, the pinned one goes on
refusing until the document re-pins it. Provenance is not sacrificed to get
there — the lax delivery still gets its report row, now stamped with the mode
it was resolved under.

Every property of the original change survives: a re-pinned ref or url still
passes, a rename still keeps its baseline, a SHA-shaped ref still trips
neither branch, and report rows are still one per declaration.

The regression test asserts the rebuilt baseline map by equality rather than
by the absence of the strict key — absent is also what an empty map gives, and
a row that silently stopped carrying its mode would satisfy the weaker form
for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
@totalfrank

Copy link
Copy Markdown
Collaborator Author

Review round 2 — one P1 found and fixed. Pushed 754329d.

Two reviewers ran on 4a576af: the Claude Code review returned no findings, and Codex found a P1 that was real and that I introduced. My round-1 all-clear above stands corrected — it was posted before the Codex review landed, and it was wrong to that extent.

The finding. Keying baselines on (url, ref) alone let one declaration of a repository disarm another's pin. A document may legally declare the same (url, ref) twice — once strict, once non_strict — when some entries may follow a branch and one may not. When the ref moves, the lax declaration delivers the new commit and records it while the pinned one refuses; sharing a baseline, that recorded sha became the pin's baseline, so the next apply with an unchanged document handed the pinned entry the commit it had just rejected. Strict mode degraded to "refuse each move exactly once, then deliver it" — the failure the adopt-after-the-gate ordering exists to prevent, arriving sideways through a second declaration one apply later. I reproduced it with a probe before changing anything.

This was a regression specific to this PR: before it, the two aliases had separate name-keyed baselines and could not interfere.

The fix. mode joins the key, which is now (url, ref, mode), on the rule that a pin may only be advanced by an apply that stood behind it under the same mode. The declarations keep separate histories — the lax one advances and notes its moves, the pinned one refuses until the document re-pins it. SourceResolution.mode rides through as_dict(), the stored-report decode and the response model alongside url, so the lax delivery keeps its report row rather than losing provenance to protect the pin.

This deviates from the task spec, which said baselines are keyed on (url, ref). That spec has the hole above; I judged closing it to be the intent rather than the letter, and flag it here so a human can overrule me. The reviewer's alternative — rejecting conflicting modes for one pair — I passed on because ${BOT_*} substitution happens at apply time, so the check could not live at PUT validation where an author would want it, and it would fail documents that work today. Reasoning in full on the thread.

Every property of the original change survives: re-pinning ref or url still passes, a rename keeps its baseline, a SHA-shaped ref trips neither branch, report rows stay one per declaration.

Validation. 1091 tests pass locally, including the architecture guard. New conformance case test_a_non_strict_alias_cannot_advance_a_strict_pin drives the full two-apply sequence through the front door; I checked it genuinely catches the regression by removing mode=spec.mode from adopt and watching it fail. config_manifest_apply_service.py is back to exactly 1000 lines, still at the cap.


Generated by Claude Code

@totalfrank totalfrank left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up round. Head is still 754329d — the same commit my last review (and the reply to the Codex thread) was already posted against, so there are no new commits to review this round.

Findings checklist:

  • Codex P1 — non-strict alias could advance a strict pin's baseline (config_manifest_apply_service.py#L792-793): fixed in 754329d, verified by reading the current code (mode is the third element of SourceSession.baselines's key throughout source_session.py, source_fetchers.py, and _last_resolutions()) and the new regression test test_a_non_strict_alias_cannot_advance_a_strict_pin. Thread resolved.
  • My own full-diff review (dd835cd..4a576af): clean, no findings.

Open: 0. PR is clean as of 754329d.


Generated by Claude Code

@totalfrank

Copy link
Copy Markdown
Collaborator Author

@codex review

Re-requesting explicitly because a push is not one of your triggers: your P1 on the shared strict/non_strict baseline was fixed in 754329d, which landed after your review of 4a576af. The fix changes the baseline key to (url, ref, mode) and touches source_session.py, source_fetchers.py, config_manifest_apply_service.py, outcomes.py, the report codec and the response model — worth a look at the new head rather than leaving the fix for your own finding unverified.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 754329d753

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The same alias hazard as the last commit, surviving on the road where the
display is not unique. Codex caught it on the new head; a probe confirmed it.

`adopt` de-duplicated on the display alone. A named source's display is its
`from` name, which maps to exactly one `(url, ref, mode)`, so it was fine. An
inline source's display is `url@ref` — mode-blind — so two inline declarations
of one repository at one ref but two modes shared it. Whichever resolved first
took the slot and the other recorded nothing; when the loser was the `strict`
one it never established a baseline, and a pin with no baseline never refuses
anything. That is worse than the bug it followed: not "refuse once then
deliver", but a pin disarmed from the first apply onward.

The rule that closes it, and the one to check against next time a key or an
identity moves: **the recording identity must be at least as fine as the key
the recording feeds.** The baseline key is `(url, ref, mode)`, so the
recording identity is now `(display, mode)`.

Both declarations get a report row. They share a name, because `url@ref` is
what an inline source is called, and are told apart by the mode each was
resolved under.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
@totalfrank

Copy link
Copy Markdown
Collaborator Author

Review round 3 — one P1 found and fixed. Pushed 54ff99bc.

Codex re-reviewed 754329d and found that my round-2 fix was incomplete: the same alias hazard survived on the inline road. Round 2's mode-in-the-key change fixed named aliases and left inline ones collapsed.

The finding. adopt de-duplicated on the display alone. A named source's display is its from name, which maps to exactly one (url, ref, mode) — fine. An inline source's display is url@ref, which is mode-blind, so two inline declarations of one repository at one ref but two modes shared it. Whichever resolved first took the slot and the other recorded nothing. When the loser was the strict one it never established a baseline at all — and a pin with no baseline never refuses anything. That is worse than the bug it followed: not "refuse each move once, then deliver it", but a pin disarmed from the first apply onward and permanently.

The fix. adopt de-duplicates on (display, mode). I preferred this to folding the mode into the inline display because the display is user-facing — it is the report row's name and appears in the strict-refusal message — and url@ref is the form the schema doc now promises. Both declarations get a row; they share the name and are distinguished by mode.

The invariant I should have drawn a commit earlier, now written into _recorded's docstring: the recording identity must be at least as fine as the key the recording feeds. Round 2 moved the baseline key to (url, ref, mode) and left the recording identity at display. That is exactly why the named case looked right and the inline case did not, and it is the thing to check the next time either side moves.

Validation. 1085 tests pass locally including the architecture guard; the service file is still at exactly 1000 lines. New conformance case test_two_inline_declarations_at_two_modes_are_two_rows covers both halves — the first-apply recording, and the refusal on the apply after the ref moves. As with the round-2 test, I verified it genuinely catches the regression by collapsing the de-dup key back to a constant mode and watching it fail.

Worth noting for the human reviewers: a push does not re-trigger Codex — its triggers are open-for-review, draft→ready, and an explicit @codex review comment. Round 2's fix would have gone unverified by it had I not asked. I'll keep re-requesting after each push until a round comes back clean from both reviewers.

@codex review


Generated by Claude Code

@totalfrank totalfrank left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 4 — housekeeping only, no new commits.

This run's synchronize webhook fired for 54ff99bc, which is the same commit round 3 (the previous run) already reviewed and posted against — so there is nothing new between that review and this head to diff or re-review.

What this round did: verified the one review thread that round 3 had replied to as fixed but never called resolve_thread on, then resolved it.

Findings checklist (all rounds):

  • Round 1 full-diff review (dd835cd..4a576af): clean, no findings.
  • Codex P1 — non-strict alias could advance a strict pin's baseline (config_manifest_apply_service.py, (url, ref) key too coarse): fixed in 754329d (mode joins the baseline key). Verified in round 2, thread resolved.
  • Codex P1 — two inline declarations at one (url, ref) but different modes collapsed onto one report row via adopt()'s display-only de-dup, so a strict alias could lose its baseline entirely: fixed in 54ff99bc (adopt() now keys on (display, mode)). Re-verified this round by reading source_session.py (_recorded keyed on (display, spec.mode)) and both source_fetchers.py gate call sites (session.baseline(spec.url, spec.ref, spec.mode)), and confirming test_two_inline_declarations_at_two_modes_are_two_rows exercises the exact scenario. Thread resolved this round.

Open: 0. PR is clean as of 54ff99bc (cleared functionally in round 3; this round only closed the housekeeping gap of an unresolved thread).

CI on 54ff99bc: 8/10 check runs green, Backend unit tests and Singlebox coverage still in progress at review time — nothing to flag there yet, just not finished.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54ff99bc26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Third finding of one root cause, and the last shape that can have this hole:
the recording identity now contains the baseline key outright instead of
something believed to imply it.

`adopt` keyed on `(display, mode)`. An inline display is `url@ref`, and that
join is not injective — `@` is legal in a URL path and legal in a refname — so
`url=".../a@b", ref="c"` and `url=".../a", ref="b@c"` share the display
`.../a@b@c` while having different baseline keys. The second declaration was
dropped, and a dropped `strict` declaration never establishes a baseline, so
it accepts every subsequent move.

The key is now `(display, url, ref, mode)`. `display` stays because it is not
implied by the rest either: two `from` names over one `(url, ref, mode)` are
two declarations and must stay two rows.

Both narrower shapes and why each failed are recorded on `_recorded`, so the
next person to touch this has the counterexamples rather than the conclusion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8YX8W4uDYiHjcX9iRBLje
@totalfrank

Copy link
Copy Markdown
Collaborator Author

Review round 4 — one P1 found and fixed. Pushed 25bcb7dc.

Codex found the third instance of a single root cause. Worth stating as one story rather than three, because the pattern matters more than any of the individual bugs:

Round Baseline key Recording identity Hole
1 (url, ref) display a non_strict alias advanced a strict pin's baseline
2 (url, ref, mode) display inline aliases collapse — display is mode-blind, so a strict pin never records a baseline
3 (url, ref, mode) (display, mode) the @ join is not injective, so two distinct (url, ref) pairs still collapse
now (url, ref, mode) (display, url, ref, mode)

The finding. @ is legal in a URL path and legal in a refname, so url=".../a@b", ref="c" and url=".../a", ref="b@c" share the display .../a@b@c while having different baseline keys. The second declaration was dropped, and a dropped strict declaration never establishes a baseline, so it accepts every subsequent move.

The fix is Codex's suggested key verbatim: (display, url, ref, mode). display stays because it is not implied by the rest either — two from names over one (url, ref, mode) are two declarations and must stay two rows.

What I got wrong, twice. After round 3 I wrote the invariant into the code: the recording identity must be at least as fine as the key the recording feeds. Then I satisfied it with (display, mode) — a shape that only appears to imply the key. Reasoning about what a display implies is the error; putting the key in the tuple is the fix. Both narrower shapes and the counterexample that killed each are now recorded on _recorded, so the next person sees the counterexamples and not just the conclusion.

Validation. 1086 tests pass locally including the architecture guard; the service file is still at exactly 1000 lines. New conformance case test_an_at_sign_in_a_url_or_ref_does_not_collide_two_declarations covers the collision, the rebuilt baseline map, and the refusal after a move. As with the previous two, I verified it genuinely catches the regression by collapsing the key back to the old shape and watching it fail.

All three P1 threads are resolved or replied to. CI on 25bcb7dc is running.

@codex review


Generated by Claude Code

@totalfrank totalfrank left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review round 5 — housekeeping only, no new commits.

This run's synchronize webhook fired for 25bcb7dc, which is the same commit round 4 (the previous run) already reviewed and posted against — so there is nothing new between that review and this head to diff or re-review.

What this round did: re-verified round 4's fix by reading the current code rather than trusting the reply, then closed a housekeeping gap — the third P1 thread had a reply saying "Resolving" but resolve_thread was never actually called, so it was still open on GitHub. Verified apply/source_session.py's _recorded set is keyed on (display, spec.url, spec.ref, spec.mode) (source_session.py:185,264) exactly as the reply claimed, and that test_an_at_sign_in_a_url_or_ref_does_not_collide_two_declarations (test_source_resolver.py:747) exercises the collision, the two-row recording, and the post-move refusal correctly. Thread resolved this round.

Findings checklist (all rounds):

  • Round 1 full-diff review (dd835cd..4a576af): clean, no findings.
  • Codex P1 — non-strict alias could advance a strict pin's baseline (config_manifest_apply_service.py, (url, ref) key too coarse): fixed in 754329d (mode joins the baseline key). Verified in round 2, thread resolved.
  • Codex P1 — two inline declarations at one (url, ref) but different modes collapsed onto one report row via adopt()'s display-only de-dup: fixed in 54ff99bc (adopt() keys on (display, mode)). Verified in round 4, thread resolved.
  • Codex P1 — the url@ref display join is not injective (@ legal in both a URL path and a refname), so two distinct (url, ref) pairs could still collide and drop a strict declaration's baseline: fixed in 25bcb7dc (_recorded keyed on (display, url, ref, mode)). Re-verified this round by reading the code directly; thread formally resolved this round (was left open by round 4 despite the fix being real and the reply confirming it).

Open: 0. PR is clean as of 25bcb7dc.

CI on 25bcb7dc: 7/9 check runs green, Backend unit tests and BCS unit tests still in progress at review time — nothing to flag there yet, just not finished.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25bcb7dc60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@totalfrank

Copy link
Copy Markdown
Collaborator Author

Review round 5 — one P1 raised, disputed, and resolved as moot. No code change.

Both reviewers have now read 25bcb7dc. The Claude Code review came back Open: 0. Codex raised a fourth P1 — "Preserve baselines from pre-upgrade reports" — and this one I did not fix, which is the first time in this loop I've said no to a Codex finding, so the reasoning is worth stating plainly.

The mechanism it describes is real and I had already documented it: _last_resolutions skips rows lacking url/mode, so a report written before those fields existed contributes no baseline, and a strict source whose ref moved in that window would deliver the moved commit once before re-arming.

It is moot in practice: there is no pre-upgrade data. This feature is still under development and has not shipped, so no stored report predates these fields. The branch is unreachable in any environment that exists, and the upgrade path it guards has no rows to lose a baseline from. Confirmed with the author before deciding.

Neither suggested remedy survives that: reconstructing legacy rows would reconstruct nothing, and failing closed would trade an empty risk for a real regression — a brand-new bot's first apply of a strict source would fail, contradicting test_strict_on_the_first_apply_has_no_opinion.

The null-skip stays for what it actually is: a total function over a nullable field, so a hand-edited or truncated row degrades to "no opinion" rather than keying a baseline on None. Thread resolved with the reasoning on it.

One correction to my own PR body while I'm here: the "already-stored reports contribute no baselines after deployment" paragraph is accurate but reads as though there is a deployed population at risk. There isn't — that sentence exists because the task asked me to state it, not because legacy rows are known to exist.

Running total across the loop: four P1s from Codex, zero from the other reviewer. Three were real and self-inflicted, all one root cause — a recording identity coarser than the key it feeds — and each of my first two fixes was itself too weak. The fourth is this one. Full escalation table in the round-4 comment.

CI on 25bcb7dc: 8/10 green at time of writing; Backend unit tests and Singlebox coverage still in progress. I'll act on either if it fails rather than leaving it.


Generated by Claude Code

@FreddieSun FreddieSun left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@totalfrank

Copy link
Copy Markdown
Collaborator Author

CI green on 25bcb7dc — closing out the review loop. All 10 check runs succeeded, including Backend unit tests (16:00:44) and Singlebox coverage (16:05:44), the two that were still running when I posted round 5. mergeable_state is now clean.

Final state: both automated reviewers have read this head, all four review threads are resolved, and @FreddieSun has approved. Nothing outstanding from me.

Summary of the five rounds, for whoever merges this:

P1s found 4, all from Codex; 0 from the Claude Code review
Real and fixed 3 — 754329d, 54ff99bc, 25bcb7dc
Disputed 1 — pre-upgrade baselines, moot (no shipped data)
Root cause of all 3 real ones one: a recording identity coarser than the key it feeds

The three real findings were a single defect that my first two fixes did not fully close — the escalation table is in the round-4 comment, and the invariant plus both failed shapes are recorded on _recorded in source_session.py so the next person gets the counterexamples rather than the conclusion.


Generated by Claude Code

@totalfrank
totalfrank merged commit 2d5dfbf into dev Sep 11, 2026
11 checks passed
@totalfrank
totalfrank deleted the claude/eager-fermat-gxxa0j branch September 11, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants